package puzzle.masyu2.data;

import puzzle.common.data.AbstractBoard;
import puzzle.common.data.HatanException;
import puzzle.common.point.Direction;
import puzzle.masyu2.data.DangoType;
import puzzle.masyu2.data.FlagHelper;
import tomoto.assertion.AssertUtil;
import tomoto.util.ArrayUtil;
import tomoto.util.FastBitSet;

public class MasyuBoard extends AbstractBoard<MasyuBoard> {
	private static final int OUT_OF_BOUND_FLAGS = 0xf0;

	private static final int DIRTY_FLAG = 0x1000;

	private static final int EXIST_FLAG_SHIFT = 0;
	private static final int FORBIDDEN_FLAG_SHIFT = 4;
	private static final int ASSUMED_FLAG_SHIFT = 8;

	private int[] m_flags;
	private FastBitSet m_dirtyFlags;
	private DangoType[] m_dangos;

	public MasyuBoard(int w, int h) {
		super(w, h);
		m_flags = new int[w * h];
		m_dirtyFlags = new FastBitSet(w * h);
		m_dangos = new DangoType[w * h];

		initFlags();
	}
	
	public MasyuBoard(MasyuBoard other) {
		super(other);
		m_flags = ArrayUtil.dup(other.m_flags);
		m_dirtyFlags = new FastBitSet(other.m_dirtyFlags);
		m_dangos = ArrayUtil.dup(other.m_dangos);
	}
	
	public MasyuBoard dup() {
		return new MasyuBoard(this);
	}

	public void copyFromImpl(MasyuBoard b) {
		System.arraycopy(b.m_flags, 0, m_flags, 0, b.m_flags.length);
		m_dirtyFlags.copyFrom(b.m_dirtyFlags);
		System.arraycopy(b.m_dangos, 0, m_dangos, 0, b.m_dangos.length);
	}
	
	private void initFlags() {
		try {
			for (int x = 0; x < width; x++) {
				setForbiddenFlag(x, 0, Direction.N);
				setForbiddenFlag(x, height - 1, Direction.S);
			}
			for (int y = 0; y < height; y++) {
				setForbiddenFlag(0, y, Direction.W);
				setForbiddenFlag(width - 1, y, Direction.E);
			}
		} catch (HatanException ex) {
			throw AssertUtil.fail();
		}
		
		m_dirtyFlags.setAll(true);
	}

	private int getFlags(int x, int y) {
		if (inRange(x, y)) {
			return m_flags[x + y * width];
		} else {
			return OUT_OF_BOUND_FLAGS;
		}
	}

	private boolean setFlags(int x, int y, int flag) {
		if ((getFlags(x, y) & flag) == flag) {
			return false;
		} else {
			assert inRange(x, y);
			m_flags[x + y * width] |= (flag | DIRTY_FLAG);
			return true;
		}
	}

	public boolean getFlagAt(int x, int y, Direction d, int shift) {
		return (getFlags(x, y) & (FlagHelper.getDirectionMask(d) << shift)) != 0;
	}

	public boolean setFlagAt(int x, int y, Direction d, int shift) {
		boolean isChanged = false;
		isChanged |= setFlags(x, y, (FlagHelper.getDirectionMask(d) << shift));
		isChanged |= setFlags(x + d.v.dx, y + d.v.dy,
				(FlagHelper.getDirectionMask(d.inv()) << shift));
		return isChanged;
	}

	public void setDangoType(int x, int y, DangoType dt) {
		assert inRange(x, y);
		m_dangos[x + y * width] = dt;
	}

	public DangoType getDangoType(int x, int y) {
		if (inRange(x, y)) {
			return m_dangos[x + y * width];
		} else {
			return DangoType.NONE;
		}
	}

	public boolean getExistFlag(int x, int y, Direction d) {
		return getFlagAt(x, y, d, EXIST_FLAG_SHIFT);
	}

	public boolean getForbiddenFlag(int x, int y, Direction d) {
		return getFlagAt(x, y, d, FORBIDDEN_FLAG_SHIFT);
	}

	public boolean setExistFlag(int x, int y, Direction d) throws HatanException {
		if (getForbiddenFlag(x, y, d)) {
			throw new HatanException();
		}
		boolean isChanged = setFlagAt(x, y, d, EXIST_FLAG_SHIFT);
		if (getLineCountAround(x, y) > 2 || getLineCountAround(x + d.v.dx, y + d.v.dy) > 2) {
			throw new HatanException();
		}
		return isChanged;
	}

	public boolean setForbiddenFlag(int x, int y, Direction d) throws HatanException {
		if (getExistFlag(x, y, d)) {
			throw new HatanException();
		}
		return setFlagAt(x, y, d, FORBIDDEN_FLAG_SHIFT);
	}

	public boolean isDirty(int x, int y) {
		return m_dirtyFlags.get(x + y * width);
	}

	public int getLineCountAround(int x, int y) {
		return FlagHelper.getNumberOfFlags(getFlags(x, y) >> EXIST_FLAG_SHIFT);
	}

	public int getForbiddenCountAround(int x, int y) {
		return FlagHelper.getNumberOfFlags(getFlags(x, y) >> FORBIDDEN_FLAG_SHIFT);
	}

	public boolean isSatisfied(int x, int y) {
		int flags = getFlags(x, y);
		return (((flags >> EXIST_FLAG_SHIFT) | (flags >> FORBIDDEN_FLAG_SHIFT)) & 0x0f) == 0x0f;
	}

	public int getExistFlags(int x, int y) {
		return (getFlags(x, y) >> EXIST_FLAG_SHIFT) & 0x0f;
	}

	public int getForbiddenFlags(int x, int y) {
		return (getFlags(x, y) >> FORBIDDEN_FLAG_SHIFT) & 0x0f;
	}

	public void setAssumed(int x, int y, Direction d) {
		setFlagAt(x, y, d, ASSUMED_FLAG_SHIFT);
	}
	
	public boolean isAssumed(int x, int y, Direction d) {
		return getFlagAt(x, y, d, ASSUMED_FLAG_SHIFT);
	}

	public void snapshotDirtyFlags() {
		m_dirtyFlags.clear();
		for (int i = 0; i < m_flags.length; i++) {
			if ((m_flags[i] & DIRTY_FLAG) != 0) {
				m_dirtyFlags.set(i, true);
				m_flags[i] &= ~DIRTY_FLAG;
			}
		}
		
		FastBitSet mask = new FastBitSet(m_dirtyFlags);
		mask.shift(-1);
		m_dirtyFlags.or(mask);
		mask.shift(+2);
		m_dirtyFlags.or(mask);
		mask.copyFrom(m_dirtyFlags);
		mask.shift(-width);
		m_dirtyFlags.or(mask);
		mask.shift(+width * 2);
		m_dirtyFlags.or(mask);
	}

}
